﻿using System;
using System.Runtime.CompilerServices;

namespace BattleCore.FixedPoint
{
    /// <summary>
    /// A Quaternion representing an orientation.
    /// </summary>
    [Serializable]
    public struct fpquaternion
    {
        /// <summary>The X component of the quaternion.</summary>
        public fp x;

        /// <summary>The Y component of the quaternion.</summary>
        public fp y;

        /// <summary>The Z component of the quaternion.</summary>
        public fp z;

        /// <summary>The W component of the quaternion.</summary>
        public fp w;

        public static readonly fpquaternion identity;

        static fpquaternion()
        {
            identity = new fpquaternion(0, 0, 0, 1);
        }

        /// <summary>
        /// Initializes a new instance of the JQuaternion structure.
        /// </summary>
        /// <param name="x">The X component of the quaternion.</param>
        /// <param name="y">The Y component of the quaternion.</param>
        /// <param name="z">The Z component of the quaternion.</param>
        /// <param name="w">The W component of the quaternion.</param>
        public fpquaternion(fp x, fp y, fp z, fp w)
        {
            this.x = x;
            this.y = y;
            this.z = z;
            this.w = w;
        }

        public fpquaternion(quaternionInt quaternionInt)
        {
            x = quaternionInt.x;
            y = quaternionInt.y;
            z = quaternionInt.z;
            w = quaternionInt.w;
        }


        //[MethodImpl(MethodImplOptions.AggressiveInlining)]
        //public fpquaternion(Quaternion quaternion)
        //{
        //    x = quaternion.x;
        //    y = quaternion.y;
        //    z = quaternion.z;
        //    w = quaternion.w;
        //}

        public void Set(fp new_x, fp new_y, fp new_z, fp new_w)
        {
            x = new_x;
            y = new_y;
            z = new_z;
            w = new_w;
        }

        public void SetFromToRotation(fp3 fromDirection, fp3 toDirection)
        {
            fpquaternion targetRotation = fpquaternion.FromToRotation(fromDirection, toDirection);
            this.Set(targetRotation.x, targetRotation.y, targetRotation.z, targetRotation.w);
        }

        public fp3 eulerAngles
        {
            get
            {
                var result = new fp3();

                var y_sqr = y * y;
                var t0 = -2.0f * (y_sqr + z * z) + 1.0f;
                var t1 = +2.0f * (x * y - w * z);
                var t2 = -2.0f * (x * z + w * y);
                var t3 = +2.0f * (y * z - w * x);
                var t4 = -2.0f * (x * x + y_sqr) + 1.0f;

                t2 = t2 > 1.0f ? 1.0f : t2;
                t2 = t2 < -1.0f ? -1.0f : t2;

                result.x = fp.Atan2(t3, t4) * fp.Rad2Deg;
                result.y = fp.Asin(t2) * fp.Rad2Deg;
                result.z = fp.Atan2(t1, t0) * fp.Rad2Deg;

                return result * -1;
            }
        }

        public static fp Angle(fpquaternion a, fpquaternion b)
        {
            fpquaternion aInv = fpquaternion.Inverse(a);
            fpquaternion f = b * aInv;

            fp angle = fp.Acos(f.w) * 2 * fp.Rad2Deg;

            if (angle > 180)
            {
                angle = 360 - angle;
            }

            return angle;
        }

        /// <summary>
        /// Quaternions are added.
        /// </summary>
        /// <param name="quaternion1">The first quaternion.</param>
        /// <param name="quaternion2">The second quaternion.</param>
        /// <returns>The sum of both quaternions.</returns>

        #region public static JQuaternion Add(JQuaternion quaternion1, JQuaternion quaternion2)

        public static fpquaternion Add(fpquaternion quaternion1, fpquaternion quaternion2)
        {
            Add(ref quaternion1, ref quaternion2, out var result);
            return result;
        }

        public static fpquaternion LookRotation(fp3 forward)
        {
            return CreateFromMatrix(fpmatrix.LookAt(forward, fp3.up));
        }

        public static fpquaternion LookRotation(fp3 forward, fp3 upwards)
        {
            return CreateFromMatrix(fpmatrix.LookAt(forward, upwards));
        }

        public static fpquaternion Slerp(fpquaternion from, fpquaternion to, fp t)
        {
            t = fpmath.Clamp(t, 0, 1);

            fp dot = Dot(from, to);

            if (dot < 0.0f)
            {
                to = Multiply(to, -1);
                dot = -dot;
            }

            fp halfTheta = fp.Acos(dot);

            return Multiply(
                Multiply(from, fp.Sin((1 - t) * halfTheta)) + Multiply(to, fp.Sin(t * halfTheta)),
                1 / fp.Sin(halfTheta));
        }

        public static fpquaternion RotateTowards(fpquaternion from, fpquaternion to,
            fp maxDegreesDelta)
        {
            fp dot = Dot(from, to);

            if (dot < 0.0f)
            {
                to = Multiply(to, -1);
                dot = -dot;
            }

            fp halfTheta = fp.Acos(dot);
            fp theta = halfTheta * 2;

            maxDegreesDelta *= fp.Deg2Rad;

            if (maxDegreesDelta >= theta)
            {
                return to;
            }

            maxDegreesDelta /= theta;

            return Multiply(
                Multiply(from, fp.Sin((1 - maxDegreesDelta) * halfTheta)) +
                Multiply(to, fp.Sin(maxDegreesDelta * halfTheta)), 1 / fp.Sin(halfTheta));
        }

        public static fpquaternion Euler(fp x, fp y, fp z)
        {
            x *= fp.Deg2Rad;
            y *= fp.Deg2Rad;
            z *= fp.Deg2Rad;

            CreateFromYawPitchRoll(y, x, z, out var rotation);

            return rotation;
        }

        public static fpquaternion Euler(fp3 eulerAngles)
        {
            return Euler(eulerAngles.x, eulerAngles.y, eulerAngles.z);
        }

        public static fpquaternion AngleAxis(fp angle, fp3 axis)
        {
            axis = axis * fp.Deg2Rad;
            axis.Normalize();

            fp halfAngle = angle * fp.Deg2Rad * fp.Half;

            fpquaternion rotation;
            fp sin = fp.Sin(halfAngle);

            rotation.x = axis.x * sin;
            rotation.y = axis.y * sin;
            rotation.z = axis.z * sin;
            rotation.w = fp.Cos(halfAngle);

            return rotation;
        }

        public static void CreateFromYawPitchRoll(fp yaw, fp pitch, fp roll,
            out fpquaternion result)
        {
            fp num9 = roll * fp.Half;
            fp num6 = fp.Sin(num9);
            fp num5 = fp.Cos(num9);
            fp num8 = pitch * fp.Half;
            fp num4 = fp.Sin(num8);
            fp num3 = fp.Cos(num8);
            fp num7 = yaw * fp.Half;
            fp num2 = fp.Sin(num7);
            fp num = fp.Cos(num7);
            result.x = ((num * num4) * num5) + ((num2 * num3) * num6);
            result.y = ((num2 * num3) * num5) - ((num * num4) * num6);
            result.z = ((num * num3) * num6) - ((num2 * num4) * num5);
            result.w = ((num * num3) * num5) + ((num2 * num4) * num6);
        }

        /// <summary>
        /// Quaternions are added.
        /// </summary>
        /// <param name="quaternion1">The first quaternion.</param>
        /// <param name="quaternion2">The second quaternion.</param>
        /// <param name="result">The sum of both quaternions.</param>
        public static void Add(ref fpquaternion quaternion1, ref fpquaternion quaternion2,
            out fpquaternion result)
        {
            result.x = quaternion1.x + quaternion2.x;
            result.y = quaternion1.y + quaternion2.y;
            result.z = quaternion1.z + quaternion2.z;
            result.w = quaternion1.w + quaternion2.w;
        }

        #endregion

        public static fpquaternion Conjugate(fpquaternion value)
        {
            fpquaternion quaternion;
            quaternion.x = -value.x;
            quaternion.y = -value.y;
            quaternion.z = -value.z;
            quaternion.w = value.w;
            return quaternion;
        }

        public static fp Dot(fpquaternion a, fpquaternion b)
        {
            return a.w * b.w + a.x * b.x + a.y * b.y + a.z * b.z;
        }

        public static fpquaternion Inverse(fpquaternion rotation)
        {
            fp invNorm = fp.One / ((rotation.x * rotation.x) + (rotation.y * rotation.y) +
                                   (rotation.z * rotation.z) + (rotation.w * rotation.w));
            return fpquaternion.Multiply(fpquaternion.Conjugate(rotation), invNorm);
        }

        public static fpquaternion FromToRotation(fp3 fromVector, fp3 toVector)
        {
            fp3 w = fp3.Cross(fromVector, toVector);
            fpquaternion q =
                new fpquaternion(w.x, w.y, w.z, fp3.Dot(fromVector, toVector));
            q.w += fp.Sqrt(fromVector.sqrMagnitude * toVector.sqrMagnitude);
            q.Normalize();

            return q;
        }

        public static fpquaternion Lerp(fpquaternion a, fpquaternion b, fp t)
        {
            t = fpmath.Clamp(t, fp.Zero, fp.One);

            return LerpUnclamped(a, b, t);
        }

        public static fpquaternion LerpUnclamped(fpquaternion a, fpquaternion b, fp t)
        {
            fpquaternion result =
                fpquaternion.Multiply(a, (1 - t)) + fpquaternion.Multiply(b, t);
            result.Normalize();

            return result;
        }

        /// <summary>
        /// Quaternions are subtracted.
        /// </summary>
        /// <param name="quaternion1">The first quaternion.</param>
        /// <param name="quaternion2">The second quaternion.</param>
        /// <returns>The difference of both quaternions.</returns>

        #region public static JQuaternion Subtract(JQuaternion quaternion1, JQuaternion quaternion2)

        public static fpquaternion Subtract(fpquaternion quaternion1, fpquaternion quaternion2)
        {
            Subtract(ref quaternion1, ref quaternion2, out var result);
            return result;
        }

        /// <summary>
        /// Quaternions are subtracted.
        /// </summary>
        /// <param name="quaternion1">The first quaternion.</param>
        /// <param name="quaternion2">The second quaternion.</param>
        /// <param name="result">The difference of both quaternions.</param>
        public static void Subtract(ref fpquaternion quaternion1, ref fpquaternion quaternion2,
            out fpquaternion result)
        {
            result.x = quaternion1.x - quaternion2.x;
            result.y = quaternion1.y - quaternion2.y;
            result.z = quaternion1.z - quaternion2.z;
            result.w = quaternion1.w - quaternion2.w;
        }

        #endregion

        /// <summary>
        /// Multiply two quaternions.
        /// </summary>
        /// <param name="quaternion1">The first quaternion.</param>
        /// <param name="quaternion2">The second quaternion.</param>
        /// <returns>The product of both quaternions.</returns>

        #region public static JQuaternion Multiply(JQuaternion quaternion1, JQuaternion quaternion2)

        public static fpquaternion Multiply(fpquaternion quaternion1, fpquaternion quaternion2)
        {
            Multiply(ref quaternion1, ref quaternion2, out var result);
            return result;
        }

        /// <summary>
        /// Multiply two quaternions.
        /// </summary>
        /// <param name="quaternion1">The first quaternion.</param>
        /// <param name="quaternion2">The second quaternion.</param>
        /// <param name="result">The product of both quaternions.</param>
        public static void Multiply(ref fpquaternion quaternion1, ref fpquaternion quaternion2,
            out fpquaternion result)
        {
            fp x = quaternion1.x;
            fp y = quaternion1.y;
            fp z = quaternion1.z;
            fp w = quaternion1.w;
            fp num4 = quaternion2.x;
            fp num3 = quaternion2.y;
            fp num2 = quaternion2.z;
            fp num = quaternion2.w;
            fp num12 = (y * num2) - (z * num3);
            fp num11 = (z * num4) - (x * num2);
            fp num10 = (x * num3) - (y * num4);
            fp num9 = ((x * num4) + (y * num3)) + (z * num2);
            result.x = ((x * num) + (num4 * w)) + num12;
            result.y = ((y * num) + (num3 * w)) + num11;
            result.z = ((z * num) + (num2 * w)) + num10;
            result.w = (w * num) - num9;
        }

        #endregion

        /// <summary>
        /// Scale a quaternion
        /// </summary>
        /// <param name="quaternion1">The quaternion to scale.</param>
        /// <param name="scaleFactor">Scale factor.</param>
        /// <returns>The scaled quaternion.</returns>

        #region public static JQuaternion Multiply(JQuaternion quaternion1, FP scaleFactor)

        public static fpquaternion Multiply(fpquaternion quaternion1, fp scaleFactor)
        {
            Multiply(ref quaternion1, scaleFactor, out var result);
            return result;
        }

        /// <summary>
        /// Scale a quaternion
        /// </summary>
        /// <param name="quaternion1">The quaternion to scale.</param>
        /// <param name="scaleFactor">Scale factor.</param>
        /// <param name="result">The scaled quaternion.</param>
        public static void Multiply(ref fpquaternion quaternion1, fp scaleFactor,
            out fpquaternion result)
        {
            result.x = quaternion1.x * scaleFactor;
            result.y = quaternion1.y * scaleFactor;
            result.z = quaternion1.z * scaleFactor;
            result.w = quaternion1.w * scaleFactor;
        }

        #endregion

        /// <summary>
        /// Sets the length of the quaternion to one.
        /// </summary>

        #region public void Normalize()

        public void Normalize()
        {
            fp num2 = (((this.x * this.x) + (this.y * this.y)) + (this.z * this.z)) + (this.w * this.w);
            fp num = 1 / (fp.Sqrt(num2));
            this.x *= num;
            this.y *= num;
            this.z *= num;
            this.w *= num;
        }

        #endregion

        /// <summary>
        /// Creates a quaternion from a matrix.
        /// </summary>
        /// <param name="matrix">A matrix representing an orientation.</param>
        /// <returns>JQuaternion representing an orientation.</returns>

        #region public static JQuaternion CreateFromMatrix(JMatrix matrix)

        public static fpquaternion CreateFromMatrix(fpmatrix matrix)
        {
            CreateFromMatrix(ref matrix, out var result);
            return result;
        }

        /// <summary>
        /// Creates a quaternion from a matrix.
        /// </summary>
        /// <param name="matrix">A matrix representing an orientation.</param>
        /// <param name="result">JQuaternion representing an orientation.</param>
        public static void CreateFromMatrix(ref fpmatrix matrix, out fpquaternion result)
        {
            fp num8 = (matrix.M11 + matrix.M22) + matrix.M33;
            if (num8 > fp.Zero)
            {
                fp num = fp.Sqrt((num8 + fp.One));
                result.w = num * fp.Half;
                num = fp.Half / num;
                result.x = (matrix.M23 - matrix.M32) * num;
                result.y = (matrix.M31 - matrix.M13) * num;
                result.z = (matrix.M12 - matrix.M21) * num;
            }
            else if ((matrix.M11 >= matrix.M22) && (matrix.M11 >= matrix.M33))
            {
                fp num7 = fp.Sqrt((((fp.One + matrix.M11) - matrix.M22) - matrix.M33));
                fp num4 = fp.Half / num7;
                result.x = fp.Half * num7;
                result.y = (matrix.M12 + matrix.M21) * num4;
                result.z = (matrix.M13 + matrix.M31) * num4;
                result.w = (matrix.M23 - matrix.M32) * num4;
            }
            else if (matrix.M22 > matrix.M33)
            {
                fp num6 = fp.Sqrt((((fp.One + matrix.M22) - matrix.M11) - matrix.M33));
                fp num3 = fp.Half / num6;
                result.x = (matrix.M21 + matrix.M12) * num3;
                result.y = fp.Half * num6;
                result.z = (matrix.M32 + matrix.M23) * num3;
                result.w = (matrix.M31 - matrix.M13) * num3;
            }
            else
            {
                fp num5 = fp.Sqrt((((fp.One + matrix.M33) - matrix.M11) - matrix.M22));
                fp num2 = fp.Half / num5;
                result.x = (matrix.M31 + matrix.M13) * num2;
                result.y = (matrix.M32 + matrix.M23) * num2;
                result.z = fp.Half * num5;
                result.w = (matrix.M12 - matrix.M21) * num2;
            }
        }

        #endregion

        /// <summary>
        /// Multiply two quaternions.
        /// </summary>
        /// <param name="value1">The first quaternion.</param>
        /// <param name="value2">The second quaternion.</param>
        /// <returns>The product of both quaternions.</returns>

        #region public static FP operator *(JQuaternion value1, JQuaternion value2)

        public static fpquaternion operator *(fpquaternion value1, fpquaternion value2)
        {
            Multiply(ref value1, ref value2, out var result);
            return result;
        }

        #endregion

        /// <summary>
        /// Add two quaternions.
        /// </summary>
        /// <param name="value1">The first quaternion.</param>
        /// <param name="value2">The second quaternion.</param>
        /// <returns>The sum of both quaternions.</returns>

        #region public static FP operator +(JQuaternion value1, JQuaternion value2)

        public static fpquaternion operator +(fpquaternion value1, fpquaternion value2)
        {
            Add(ref value1, ref value2, out var result);
            return result;
        }

        #endregion

        /// <summary>
        /// Subtract two quaternions.
        /// </summary>
        /// <param name="value1">The first quaternion.</param>
        /// <param name="value2">The second quaternion.</param>
        /// <returns>The difference of both quaternions.</returns>

        #region public static FP operator -(JQuaternion value1, JQuaternion value2)

        public static fpquaternion operator -(fpquaternion value1, fpquaternion value2)
        {
            Subtract(ref value1, ref value2, out var result);
            return result;
        }

        #endregion

        public static fp3 operator *(fpquaternion quat, fp3 vec)
        {
            var num = quat.x * 2f;
            var num2 = quat.y * 2f;
            var num3 = quat.z * 2f;
            var num4 = quat.x * num;
            var num5 = quat.y * num2;
            var num6 = quat.z * num3;
            var num7 = quat.x * num2;
            var num8 = quat.x * num3;
            var num9 = quat.y * num3;
            var num10 = quat.w * num;
            var num11 = quat.w * num2;
            var num12 = quat.w * num3;

            fp3 result;
            result.x = (1f - (num5 + num6)) * vec.x + (num7 - num12) * vec.y + (num8 + num11) * vec.z;
            result.y = (num7 + num12) * vec.x + (1f - (num4 + num6)) * vec.y + (num9 - num10) * vec.z;
            result.z = (num8 - num11) * vec.x + (num9 + num10) * vec.y + (1f - (num4 + num5)) * vec.z;

            return result;
        }


        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static fpquaternion operator /(fpquaternion value1, fp value2)
        {
            return new fpquaternion(value1.x / value2, value1.y / value2, value1.z / value2, value1.w / value2);
        }

        public override string ToString()
        {
            return $"({x.AsFloat():f1}, {y.AsFloat():f1}, {z.AsFloat():f1}, {w.AsFloat():f1})";
        }

        //public Quaternion ToQuaternion()
        //{
        //    return new Quaternion(x.AsFloat(), y.AsFloat(), z.AsFloat(), w.AsFloat());
        //}
    }
}